//
//  ========================================================================
//  Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.servlet;

import static org.eclipse.jetty.http.GzipHttpContent.ETAG_GZIP_QUOTE;
import static org.eclipse.jetty.http.GzipHttpContent.removeGzipFromETag;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.StringTokenizer;

import javax.servlet.AsyncContext;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.UnavailableException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.jetty.http.DateParser;
import org.eclipse.jetty.http.HttpContent;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.PathMap.MappedEntry;
import org.eclipse.jetty.http.PreEncodedHttpField;
import org.eclipse.jetty.http.QuotedCSV;
import org.eclipse.jetty.io.WriterOutputStream;
import org.eclipse.jetty.server.HttpOutput;
import org.eclipse.jetty.server.InclusiveByteRange;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.ResourceCache;
import org.eclipse.jetty.server.ResourceContentFactory;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.MultiPartOutputStream;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceCollection;
import org.eclipse.jetty.util.resource.ResourceFactory;

/** 
 * The default servlet.
 * <p>
 * This servlet, normally mapped to /, provides the handling for static
 * content, OPTION and TRACE methods for the context.
 * The following initParameters are supported, these can be set either
 * on the servlet itself or as ServletContext initParameters with a prefix
 * of org.eclipse.jetty.servlet.Default. :
 * <pre>
 *  acceptRanges      If true, range requests and responses are
 *                    supported
 *
 *  dirAllowed        If true, directory listings are returned if no
 *                    welcome file is found. Else 403 Forbidden.
 *
 *  welcomeServlets   If true, attempt to dispatch to welcome files
 *                    that are servlets, but only after no matching static
 *                    resources could be found. If false, then a welcome
 *                    file must exist on disk. If "exact", then exact
 *                    servlet matches are supported without an existing file.
 *                    Default is true.
 *
 *                    This must be false if you want directory listings,
 *                    but have index.jsp in your welcome file list.
 *
 *  redirectWelcome   If true, welcome files are redirected rather than
 *                    forwarded to.
 *
 *  gzip              If set to true, then static content will be served as
 *                    gzip content encoded if a matching resource is
 *                    found ending with ".gz"
 *
 *  resourceBase      Set to replace the context resource base
 *
 *  resourceCache     If set, this is a context attribute name, which the servlet
 *                    will use to look for a shared ResourceCache instance.
 *
 *  relativeResourceBase
 *                    Set with a pathname relative to the base of the
 *                    servlet context root. Useful for only serving static content out
 *                    of only specific subdirectories.
 *
 *  pathInfoOnly      If true, only the path info will be applied to the resourceBase
 *
 *  stylesheet        Set with the location of an optional stylesheet that will be used
 *                    to decorate the directory listing html.
 *
 *  etags             If True, weak etags will be generated and handled.
 *
 *  maxCacheSize      The maximum total size of the cache or 0 for no cache.
 *  maxCachedFileSize The maximum size of a file to cache
 *  maxCachedFiles    The maximum number of files to cache
 *
 *  useFileMappedBuffer
 *                    If set to true, it will use mapped file buffer to serve static content
 *                    when using NIO connector. Setting this value to false means that
 *                    a direct buffer will be used instead of a mapped file buffer.
 *                    This is set to false by default by this class, but may be overridden
 *                    by eg webdefault.xml 
 *
 *  cacheControl      If set, all static content will have this value set as the cache-control
 *                    header.
 *                    
 * otherGzipFileExtensions
 *                    Other file extensions that signify that a file is gzip compressed. Eg ".svgz"
 *
 *
 * </pre>
 *
 */
@SuppressWarnings("deprecation")
public class DefaultServlet extends HttpServlet implements ResourceFactory
{
    private static final Logger LOG = Log.getLogger(DefaultServlet.class);
    
    private static final long serialVersionUID = 4930458713846881193L;
    
    private static final PreEncodedHttpField ACCEPT_RANGES = new PreEncodedHttpField(HttpHeader.ACCEPT_RANGES, "bytes");
    
    private ServletContext _servletContext;
    
    private ContextHandler _contextHandler;
    
    private boolean _acceptRanges = true;
    
    private boolean _dirAllowed = true;
    
    private boolean _welcomeServlets = false;
    
    private boolean _welcomeExactServlets = false;
    
    private boolean _redirectWelcome = false;
    
    private boolean _gzip = false;
    
    private boolean _pathInfoOnly = false;
    
    private boolean _etags = false;
    
    private Resource _resourceBase;
    
    private ResourceCache _cache;
    
    private HttpContent.Factory _contentFactory;
    
    private MimeTypes _mimeTypes;
    
    private String[] _welcomes;
    
    private Resource _stylesheet;
    
    private boolean _useFileMappedBuffer = false;
    
    private HttpField _cacheControl;
    
    private String _relativeResourceBase;
    
    private ServletHandler _servletHandler;
    
    private ServletHolder _defaultHolder;
    
    private List<String> _gzipEquivalentFileExtensions;
    
    /* ------------------------------------------------------------ */
    @Override
    public void init()
        throws UnavailableException
    {
        _servletContext = getServletContext();
        _contextHandler = initContextHandler(_servletContext);
        
        _mimeTypes = _contextHandler.getMimeTypes();
        
        _welcomes = _contextHandler.getWelcomeFiles();
        if (_welcomes == null)
            _welcomes = new String[] {"index.html", "index.jsp"};
            
        _acceptRanges = getInitBoolean("acceptRanges", _acceptRanges);
        _dirAllowed = getInitBoolean("dirAllowed", _dirAllowed);
        _redirectWelcome = getInitBoolean("redirectWelcome", _redirectWelcome);
        _gzip = getInitBoolean("gzip", _gzip);
        _pathInfoOnly = getInitBoolean("pathInfoOnly", _pathInfoOnly);
        
        if ("exact".equals(getInitParameter("welcomeServlets")))
        {
            _welcomeExactServlets = true;
            _welcomeServlets = false;
        }
        else
            _welcomeServlets = getInitBoolean("welcomeServlets", _welcomeServlets);
            
        //        _useFileMappedBuffer = getInitBoolean("useFileMappedBuffer", _useFileMappedBuffer);
        _useFileMappedBuffer = false;// HERE patch by lnwazg 2016-4-19 关闭windows映射缓存
        
        _relativeResourceBase = getInitParameter("relativeResourceBase");
        
        String rb = getInitParameter("resourceBase");
        if (rb != null)
        {
            if (_relativeResourceBase != null)
                throw new UnavailableException("resourceBase & relativeResourceBase");
            try
            {
                _resourceBase = _contextHandler.newResource(rb);
            }
            catch (Exception e)
            {
                LOG.warn(Log.EXCEPTION, e);
                throw new UnavailableException(e.toString());
            }
        }
        
        String css = getInitParameter("stylesheet");
        try
        {
            if (css != null)
            {
                _stylesheet = Resource.newResource(css);
                if (!_stylesheet.exists())
                {
                    LOG.warn("!" + css);
                    _stylesheet = null;
                }
            }
            if (_stylesheet == null)
            {
                _stylesheet = Resource.newResource(this.getClass().getResource("/jetty-dir.css"));
            }
        }
        catch (Exception e)
        {
            LOG.warn(e.toString());
            LOG.debug(e);
        }
        
        String cc = getInitParameter("cacheControl");
        if (cc != null)
            _cacheControl = new PreEncodedHttpField(HttpHeader.CACHE_CONTROL, cc);
            
        String resourceCache = getInitParameter("resourceCache");
        int max_cache_size = getInitInt("maxCacheSize", -2);
        int max_cached_file_size = getInitInt("maxCachedFileSize", -2);
        int max_cached_files = getInitInt("maxCachedFiles", -2);
        if (resourceCache != null)
        {
            if (max_cache_size != -1 || max_cached_file_size != -2 || max_cached_files != -2)
                LOG.debug("ignoring resource cache configuration, using resourceCache attribute");
            if (_relativeResourceBase != null || _resourceBase != null)
                throw new UnavailableException("resourceCache specified with resource bases");
            _cache = (ResourceCache)_servletContext.getAttribute(resourceCache);
            
            if (LOG.isDebugEnabled())
                LOG.debug("Cache {}={}", resourceCache, _contentFactory);
        }
        
        _etags = getInitBoolean("etags", _etags);
        
        try
        {
            if (_cache == null && (max_cached_files != -2 || max_cache_size != -2 || max_cached_file_size != -2))
            {
                _cache = new ResourceCache(null, this, _mimeTypes, _useFileMappedBuffer, _etags, _gzip);
                if (max_cache_size >= 0)
                    _cache.setMaxCacheSize(max_cache_size);
                if (max_cached_file_size >= -1)
                    _cache.setMaxCachedFileSize(max_cached_file_size);
                if (max_cached_files >= -1)
                    _cache.setMaxCachedFiles(max_cached_files);
                _servletContext.setAttribute(resourceCache == null ? "resourceCache" : resourceCache, _cache);
            }
        }
        catch (Exception e)
        {
            LOG.warn(Log.EXCEPTION, e);
            throw new UnavailableException(e.toString());
        }
        
        if (_cache != null)
            _contentFactory = _cache;
        else
        {
            _contentFactory = new ResourceContentFactory(this, _mimeTypes, _gzip);
            if (resourceCache != null)
                _servletContext.setAttribute(resourceCache, _contentFactory);
        }
        
        _gzipEquivalentFileExtensions = new ArrayList<String>();
        String otherGzipExtensions = getInitParameter("otherGzipFileExtensions");
        if (otherGzipExtensions != null)
        {
            //comma separated list
            StringTokenizer tok = new StringTokenizer(otherGzipExtensions, ",", false);
            while (tok.hasMoreTokens())
            {
                String s = tok.nextToken().trim();
                _gzipEquivalentFileExtensions.add((s.charAt(0) == '.' ? s : "." + s));
            }
        }
        else
        {
            //.svgz files are gzipped svg files and must be served with Content-Encoding:gzip
            _gzipEquivalentFileExtensions.add(".svgz");
        }
        
        _servletHandler = _contextHandler.getChildHandlerByClass(ServletHandler.class);
        for (ServletHolder h : _servletHandler.getServlets())
            if (h.getServletInstance() == this)
                _defaultHolder = h;
                
        if (LOG.isDebugEnabled())
            LOG.debug("resource base = " + _resourceBase);
    }
    
    /**
     * Compute the field _contextHandler.<br>
     * In the case where the DefaultServlet is deployed on the HttpService it is likely that
     * this method needs to be overwritten to unwrap the ServletContext facade until we reach
     * the original jetty's ContextHandler.
     * @param servletContext The servletContext of this servlet.
     * @return the jetty's ContextHandler for this servletContext.
     */
    protected ContextHandler initContextHandler(ServletContext servletContext)
    {
        ContextHandler.Context scontext = ContextHandler.getCurrentContext();
        if (scontext == null)
        {
            if (servletContext instanceof ContextHandler.Context)
                return ((ContextHandler.Context)servletContext).getContextHandler();
            else
                throw new IllegalArgumentException(
                    "The servletContext " + servletContext + " " + servletContext.getClass().getName() + " is not " + ContextHandler.Context.class.getName());
        }
        else
            return ContextHandler.getCurrentContext().getContextHandler();
    }
    
    /* ------------------------------------------------------------ */
    @Override
    public String getInitParameter(String name)
    {
        String value = getServletContext().getInitParameter("org.eclipse.jetty.servlet.Default." + name);
        if (value == null)
            value = super.getInitParameter(name);
        return value;
    }
    
    /* ------------------------------------------------------------ */
    private boolean getInitBoolean(String name, boolean dft)
    {
        String value = getInitParameter(name);
        if (value == null || value.length() == 0)
            return dft;
        return (value.startsWith("t") || value.startsWith("T") || value.startsWith("y") || value.startsWith("Y") || value.startsWith("1"));
    }
    
    /* ------------------------------------------------------------ */
    private int getInitInt(String name, int dft)
    {
        String value = getInitParameter(name);
        if (value == null)
            value = getInitParameter(name);
        if (value != null && value.length() > 0)
            return Integer.parseInt(value);
        return dft;
    }
    
    /* ------------------------------------------------------------ */
    /** get Resource to serve.
     * Map a path to a resource. The default implementation calls
     * HttpContext.getResource but derived servlets may provide
     * their own mapping.
     * @param pathInContext The path to find a resource for.
     * @return The resource to serve.
     */
    @Override
    public Resource getResource(String pathInContext)
    {
        Resource r = null;
        if (_relativeResourceBase != null)
            pathInContext = URIUtil.addPaths(_relativeResourceBase, pathInContext);
            
        try
        {
            if (_resourceBase != null)
            {
                r = _resourceBase.addPath(pathInContext);
                if (!_contextHandler.checkAlias(pathInContext, r))
                    r = null;
            }
            else if (_servletContext instanceof ContextHandler.Context)
            {
                r = _contextHandler.getResource(pathInContext);
            }
            else
            {
                URL u = _servletContext.getResource(pathInContext);
                r = _contextHandler.newResource(u);
            }
            
            if (LOG.isDebugEnabled())
                LOG.debug("Resource " + pathInContext + "=" + r);
        }
        catch (IOException e)
        {
            LOG.ignore(e);
        }
        
        if ((r == null || !r.exists()) && pathInContext.endsWith("/jetty-dir.css"))
            r = _stylesheet;
            
        return r;
    }
    
    /* ------------------------------------------------------------ */
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException
    {
        String servletPath = null;
        String pathInfo = null;
        Enumeration<String> reqRanges = null;
        boolean included = request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null;
        if (included)
        {
            servletPath = (String)request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH);
            pathInfo = (String)request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO);
            if (servletPath == null)
            {
                servletPath = request.getServletPath();
                pathInfo = request.getPathInfo();
            }
        }
        else
        {
            servletPath = _pathInfoOnly ? "/" : request.getServletPath();
            pathInfo = request.getPathInfo();
            
            // Is this a Range request?
            reqRanges = request.getHeaders(HttpHeader.RANGE.asString());
            if (!hasDefinedRange(reqRanges))
                reqRanges = null;
        }
        
        String pathInContext = URIUtil.addPaths(servletPath, pathInfo);
        boolean endsWithSlash = (pathInfo == null ? request.getServletPath() : pathInfo).endsWith(URIUtil.SLASH);
        boolean gzippable = _gzip && !endsWithSlash && !included && reqRanges == null;
        
        HttpContent content = null;
        boolean release_content = true;
        try
        {
            // Find the content
            content = _contentFactory.getContent(pathInContext, response.getBufferSize());
            if (LOG.isDebugEnabled())
                LOG.info("content={}", content);
                
            // Not found?
            if (content == null || !content.getResource().exists())
            {
                if (included)
                    throw new FileNotFoundException("!" + pathInContext);
                response.sendError(HttpServletResponse.SC_NOT_FOUND);
                return;
            }
            
            // Directory?
            if (content.getResource().isDirectory())
            {
                sendWelcome(content, pathInContext, endsWithSlash, included, request, response);
                return;
            }
            
            // Strip slash?
            if (endsWithSlash && pathInContext.length() > 1)
            {
                String q = request.getQueryString();
                pathInContext = pathInContext.substring(0, pathInContext.length() - 1);
                if (q != null && q.length() != 0)
                    pathInContext += "?" + q;
                response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(_servletContext.getContextPath(), pathInContext)));
                return;
            }
            
            // Conditional response?
            if (!included && !passConditionalHeaders(request, response, content))
                return;
                
            // Gzip?
            HttpContent gzip_content = gzippable ? content.getGzipContent() : null;
            if (gzip_content != null)
            {
                // Tell caches that response may vary by accept-encoding
                response.addHeader(HttpHeader.VARY.asString(), HttpHeader.ACCEPT_ENCODING.asString());
                
                // Does the client accept gzip?
                String accept = request.getHeader(HttpHeader.ACCEPT_ENCODING.asString());
                if (accept != null && accept.indexOf("gzip") >= 0)
                {
                    if (LOG.isDebugEnabled())
                        LOG.debug("gzip={}", gzip_content);
                    content = gzip_content;
                }
            }
            
            // TODO this should be done by HttpContent#getContentEncoding
            if (isGzippedContent(pathInContext))
                response.setHeader(HttpHeader.CONTENT_ENCODING.asString(), "gzip");
                
            // Send the data
            release_content = sendData(request, response, included, content, reqRanges);
            
        }
        catch (IllegalArgumentException e)
        {
            LOG.warn(Log.EXCEPTION, e);
            if (!response.isCommitted())
                response.sendError(500, e.getMessage());
        }
        finally
        {
            if (release_content)
            {
                if (content != null)
                    content.release();
            }
        }
        
    }
    
    protected void sendWelcome(HttpContent content, String pathInContext, boolean endsWithSlash, boolean included, HttpServletRequest request,
        HttpServletResponse response)
            throws ServletException, IOException
    {
        // Redirect to directory
        if (!endsWithSlash || (pathInContext.length() == 1 && request.getAttribute("org.eclipse.jetty.server.nullPathInfo") != null))
        {
            StringBuffer buf = request.getRequestURL();
            synchronized (buf)
            {
                int param = buf.lastIndexOf(";");
                if (param < 0)
                    buf.append('/');
                else
                    buf.insert(param, '/');
                String q = request.getQueryString();
                if (q != null && q.length() != 0)
                {
                    buf.append('?');
                    buf.append(q);
                }
                response.setContentLength(0);
                response.sendRedirect(response.encodeRedirectURL(buf.toString()));
            }
            return;
        }
        
        // look for a welcome file
        String welcome = getWelcomeFile(pathInContext);
        if (welcome != null)
        {
            if (LOG.isDebugEnabled())
                LOG.debug("welcome={}", welcome);
            if (_redirectWelcome)
            {
                // Redirect to the index
                response.setContentLength(0);
                String q = request.getQueryString();
                if (q != null && q.length() != 0)
                    response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(_servletContext.getContextPath(), welcome) + "?" + q));
                else
                    response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(_servletContext.getContextPath(), welcome)));
            }
            else
            {
                // Forward to the index
                RequestDispatcher dispatcher = request.getRequestDispatcher(welcome);
                if (dispatcher != null)
                {
                    if (included)
                        dispatcher.include(request, response);
                    else
                    {
                        request.setAttribute("org.eclipse.jetty.server.welcome", welcome);
                        dispatcher.forward(request, response);
                    }
                }
            }
            return;
        }
        
        if (included || passConditionalHeaders(request, response, content))
            sendDirectory(request, response, content.getResource(), pathInContext);
    }
    
    /* ------------------------------------------------------------ */
    protected boolean isGzippedContent(String path)
    {
        if (path == null)
            return false;
            
        for (String suffix : _gzipEquivalentFileExtensions)
            if (path.endsWith(suffix))
                return true;
        return false;
    }
    
    /* ------------------------------------------------------------ */
    private boolean hasDefinedRange(Enumeration<String> reqRanges)
    {
        return (reqRanges != null && reqRanges.hasMoreElements());
    }
    
    /* ------------------------------------------------------------ */
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException
    {
        doGet(request, response);
    }
    
    /* ------------------------------------------------------------ */
    /* (non-Javadoc)
     * @see javax.servlet.http.HttpServlet#doTrace(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
     */
    @Override
    protected void doTrace(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException
    {
        resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
    }
    
    /* ------------------------------------------------------------ */
    @Override
    protected void doOptions(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException
    {
        resp.setHeader("Allow", "GET,HEAD,POST,OPTIONS");
    }
    
    /* ------------------------------------------------------------ */
    /**
     * Finds a matching welcome file for the supplied {@link Resource}. This will be the first entry in the list of
     * configured {@link #_welcomes welcome files} that existing within the directory referenced by the <code>Resource</code>.
     * If the resource is not a directory, or no matching file is found, then it may look for a valid servlet mapping.
     * If there is none, then <code>null</code> is returned.
     * The list of welcome files is read from the {@link ContextHandler} for this servlet, or
     * <code>"index.jsp" , "index.html"</code> if that is <code>null</code>.
     * @param resource
     * @return The path of the matching welcome file in context or null.
     * @throws IOException
     * @throws MalformedURLException
     */
    private String getWelcomeFile(String pathInContext)
        throws MalformedURLException, IOException
    {
        if (_welcomes == null)
            return null;
            
        String welcome_servlet = null;
        for (int i = 0; i < _welcomes.length; i++)
        {
            String welcome_in_context = URIUtil.addPaths(pathInContext, _welcomes[i]);
            Resource welcome = getResource(welcome_in_context);
            if (welcome != null && welcome.exists())
                return _welcomes[i];
                
            if ((_welcomeServlets || _welcomeExactServlets) && welcome_servlet == null)
            {
                MappedEntry<?> entry = _servletHandler.getHolderEntry(welcome_in_context);
                if (entry != null && entry.getValue() != _defaultHolder
                    && (_welcomeServlets || (_welcomeExactServlets && entry.getKey().equals(welcome_in_context))))
                    welcome_servlet = welcome_in_context;
                    
            }
        }
        return welcome_servlet;
    }
    
    /* ------------------------------------------------------------ */
    /* Check modification date headers.
     */
    protected boolean passConditionalHeaders(HttpServletRequest request, HttpServletResponse response, HttpContent content)
        throws IOException
    {
        try
        {
            String ifm = null;
            String ifnm = null;
            String ifms = null;
            long ifums = -1;
            
            if (request instanceof Request)
            {
                // Find multiple fields by iteration as an optimization 
                HttpFields fields = ((Request)request).getHttpFields();
                for (int i = fields.size(); i-- > 0;)
                {
                    HttpField field = fields.getField(i);
                    if (field.getHeader() != null)
                    {
                        switch (field.getHeader())
                        {
                            case IF_MATCH:
                                ifm = field.getValue();
                                break;
                            case IF_NONE_MATCH:
                                ifnm = field.getValue();
                                break;
                            case IF_MODIFIED_SINCE:
                                ifms = field.getValue();
                                break;
                            case IF_UNMODIFIED_SINCE:
                                ifums = DateParser.parseDate(field.getValue());
                                break;
                            default:
                        }
                    }
                }
            }
            else
            {
                ifm = request.getHeader(HttpHeader.IF_MATCH.asString());
                ifnm = request.getHeader(HttpHeader.IF_NONE_MATCH.asString());
                ifms = request.getHeader(HttpHeader.IF_MODIFIED_SINCE.asString());
                ifums = request.getDateHeader(HttpHeader.IF_UNMODIFIED_SINCE.asString());
            }
            
            if (!HttpMethod.HEAD.is(request.getMethod()))
            {
                if (_etags)
                {
                    String etag = content.getETagValue();
                    if (ifm != null)
                    {
                        boolean match = false;
                        if (etag != null)
                        {
                            QuotedCSV quoted = new QuotedCSV(true, ifm);
                            for (String tag : quoted)
                            {
                                if (etag.equals(tag) || tag.endsWith(ETAG_GZIP_QUOTE) && etag.equals(removeGzipFromETag(tag)))
                                {
                                    match = true;
                                    break;
                                }
                            }
                        }
                        
                        if (!match)
                        {
                            response.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED);
                            return false;
                        }
                    }
                    
                    if (ifnm != null && etag != null)
                    {
                        // Handle special case of exact match OR gzip exact match
                        if (etag.equals(ifnm) || ifnm.endsWith(ETAG_GZIP_QUOTE) && ifnm.indexOf(',') < 0 && etag.equals(removeGzipFromETag(etag)))
                        {
                            response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                            response.setHeader(HttpHeader.ETAG.asString(), ifnm);
                            return false;
                        }
                        
                        // Handle list of tags
                        QuotedCSV quoted = new QuotedCSV(true, ifnm);
                        for (String tag : quoted)
                        {
                            if (etag.equals(tag) || tag.endsWith(ETAG_GZIP_QUOTE) && etag.equals(removeGzipFromETag(tag)))
                            {
                                response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                                response.setHeader(HttpHeader.ETAG.asString(), tag);
                                return false;
                            }
                        }
                        
                        // If etag requires content to be served, then do not check if-modified-since
                        return true;
                    }
                }
                
                // Handle if modified since
                if (ifms != null)
                {
                    //Get jetty's Response impl
                    String mdlm = content.getLastModifiedValue();
                    if (mdlm != null && ifms.equals(mdlm))
                    {
                        response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                        if (_etags)
                            response.setHeader(HttpHeader.ETAG.asString(), content.getETagValue());
                        response.flushBuffer();
                        return false;
                    }
                    
                    long ifmsl = request.getDateHeader(HttpHeader.IF_MODIFIED_SINCE.asString());
                    if (ifmsl != -1 && content.getResource().lastModified() / 1000 <= ifmsl / 1000)
                    {
                        response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                        if (_etags)
                            response.setHeader(HttpHeader.ETAG.asString(), content.getETagValue());
                        response.flushBuffer();
                        return false;
                    }
                }
                
                // Parse the if[un]modified dates and compare to resource
                if (ifums != -1 && content.getResource().lastModified() / 1000 > ifums / 1000)
                {
                    response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
                    return false;
                }
                
            }
        }
        catch (IllegalArgumentException iae)
        {
            if (!response.isCommitted())
                response.sendError(400, iae.getMessage());
            throw iae;
        }
        return true;
    }
    
    /* ------------------------------------------------------------------- */
    protected void sendDirectory(HttpServletRequest request, HttpServletResponse response, Resource resource, String pathInContext)
        throws IOException
    {
        if (!_dirAllowed)
        {
            response.sendError(HttpServletResponse.SC_FORBIDDEN);
            return;
        }
        
        byte[] data = null;
        String base = URIUtil.addPaths(request.getRequestURI(), URIUtil.SLASH);
        
        //If the DefaultServlet has a resource base set, use it
        if (_resourceBase != null)
        {
            // handle ResourceCollection
            if (_resourceBase instanceof ResourceCollection)
                resource = _resourceBase.addPath(pathInContext);
        }
        //Otherwise, try using the resource base of its enclosing context handler
        else if (_contextHandler.getBaseResource() instanceof ResourceCollection)
            resource = _contextHandler.getBaseResource().addPath(pathInContext);
            
        String dir = resource.getListHTML(base, pathInContext.length() > 1);
        if (dir == null)
        {
            response.sendError(HttpServletResponse.SC_FORBIDDEN, "No directory");
            return;
        }
        
        data = dir.getBytes("utf-8");
        response.setContentType("text/html;charset=utf-8");
        response.setContentLength(data.length);
        response.getOutputStream().write(data);
    }
    
    /* ------------------------------------------------------------ */
    protected boolean sendData(HttpServletRequest request, HttpServletResponse response, boolean include, final HttpContent content,
        Enumeration<String> reqRanges)
            throws IOException
    {
        final long content_length = content.getContentLengthValue();
        
        // Get the output stream (or writer)
        OutputStream out = null;
        boolean written;
        try
        {
            out = response.getOutputStream();
            
            // has something already written to the response?
            written = out instanceof HttpOutput ? ((HttpOutput)out).isWritten() : true;
        }
        catch (IllegalStateException e)
        {
            out = new WriterOutputStream(response.getWriter());
            written = true; // there may be data in writer buffer, so assume written
        }
        
        if (LOG.isDebugEnabled())
            LOG.debug(String.format("sendData content=%s out=%s async=%b", content, out, request.isAsyncSupported()));
            
        if (reqRanges == null || !reqRanges.hasMoreElements() || content_length < 0)
        {
            //  if there were no ranges, send entire entity
            if (include)
            {
                // write without headers
                content.getResource().writeTo(out, 0, content_length);
            }
            // else if we can't do a bypass write because of wrapping
            else if (written || !(out instanceof HttpOutput))
            {
                // write normally
                putHeaders(response, content, written ? -1 : 0);
                ByteBuffer buffer = content.getIndirectBuffer();
                if (buffer != null)
                    BufferUtil.writeTo(buffer, out);
                else
                    content.getResource().writeTo(out, 0, content_length);
            }
            // else do a bypass write
            else
            {
                // write the headers
                putHeaders(response, content, 0);
                
                // write the content asynchronously if supported
                if (request.isAsyncSupported())
                {
                    final AsyncContext context = request.startAsync();
                    context.setTimeout(0);
                    
                    ((HttpOutput)out).sendContent(content, new Callback()
                    {
                        @Override
                        public void succeeded()
                        {
                            context.complete();
                            content.release();
                        }
                        
                        @Override
                        public void failed(Throwable x)
                        {
                            if (x instanceof IOException)
                                LOG.debug(x);
                            else
                                LOG.warn(x);
                            context.complete();
                            content.release();
                        }
                        
                        @Override
                        public String toString()
                        {
                            return String.format("DefaultServlet@%x$CB", DefaultServlet.this.hashCode());
                        }
                    });
                    return false;
                }
                // otherwise write content blocking
                ((HttpOutput)out).sendContent(content);
            }
        }
        else
        {
            // Parse the satisfiable ranges
            List<InclusiveByteRange> ranges = InclusiveByteRange.satisfiableRanges(reqRanges, content_length);
            
            //  if there are no satisfiable ranges, send 416 response
            if (ranges == null || ranges.size() == 0)
            {
                putHeaders(response, content, 0);
                response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
                response.setHeader(HttpHeader.CONTENT_RANGE.asString(), InclusiveByteRange.to416HeaderRangeString(content_length));
                content.getResource().writeTo(out, 0, content_length);
                return true;
            }
            
            //  if there is only a single valid range (must be satisfiable
            //  since were here now), send that range with a 216 response
            if (ranges.size() == 1)
            {
                InclusiveByteRange singleSatisfiableRange = ranges.get(0);
                long singleLength = singleSatisfiableRange.getSize(content_length);
                putHeaders(response, content, singleLength);
                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
                if (!response.containsHeader(HttpHeader.DATE.asString()))
                    response.addDateHeader(HttpHeader.DATE.asString(), System.currentTimeMillis());
                response.setHeader(HttpHeader.CONTENT_RANGE.asString(), singleSatisfiableRange.toHeaderRangeString(content_length));
                content.getResource().writeTo(out, singleSatisfiableRange.getFirst(content_length), singleLength);
                return true;
            }
            
            //  multiple non-overlapping valid ranges cause a multipart
            //  216 response which does not require an overall
            //  content-length header
            //
            putHeaders(response, content, -1);
            String mimetype = (content == null ? null : content.getContentTypeValue());
            if (mimetype == null)
                LOG.warn("Unknown mimetype for " + request.getRequestURI());
            MultiPartOutputStream multi = new MultiPartOutputStream(out);
            response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
            if (!response.containsHeader(HttpHeader.DATE.asString()))
                response.addDateHeader(HttpHeader.DATE.asString(), System.currentTimeMillis());
                
            // If the request has a "Request-Range" header then we need to
            // send an old style multipart/x-byteranges Content-Type. This
            // keeps Netscape and acrobat happy. This is what Apache does.
            String ctp;
            if (request.getHeader(HttpHeader.REQUEST_RANGE.asString()) != null)
                ctp = "multipart/x-byteranges; boundary=";
            else
                ctp = "multipart/byteranges; boundary=";
            response.setContentType(ctp + multi.getBoundary());
            
            InputStream in = content.getResource().getInputStream();
            long pos = 0;
            
            // calculate the content-length
            int length = 0;
            String[] header = new String[ranges.size()];
            for (int i = 0; i < ranges.size(); i++)
            {
                InclusiveByteRange ibr = ranges.get(i);
                header[i] = ibr.toHeaderRangeString(content_length);
                length += ((i > 0) ? 2 : 0) + 2 + multi.getBoundary().length() + 2
                    + (mimetype == null ? 0 : HttpHeader.CONTENT_TYPE.asString().length() + 2 + mimetype.length()) + 2
                    + HttpHeader.CONTENT_RANGE.asString().length() + 2 + header[i].length() + 2 + 2
                    + (ibr.getLast(content_length) - ibr.getFirst(content_length)) + 1;
            }
            length += 2 + 2 + multi.getBoundary().length() + 2 + 2;
            response.setContentLength(length);
            
            for (int i = 0; i < ranges.size(); i++)
            {
                InclusiveByteRange ibr = ranges.get(i);
                multi.startPart(mimetype, new String[] {HttpHeader.CONTENT_RANGE + ": " + header[i]});
                
                long start = ibr.getFirst(content_length);
                long size = ibr.getSize(content_length);
                if (in != null)
                {
                    // Handle non cached resource
                    if (start < pos)
                    {
                        in.close();
                        in = content.getResource().getInputStream();
                        pos = 0;
                    }
                    if (pos < start)
                    {
                        in.skip(start - pos);
                        pos = start;
                    }
                    
                    IO.copy(in, multi, size);
                    pos += size;
                }
                else
                    // Handle cached resource
                    content.getResource().writeTo(multi, start, size);
            }
            if (in != null)
                in.close();
            multi.close();
        }
        return true;
    }
    
    /* ------------------------------------------------------------ */
    protected void putHeaders(HttpServletResponse response, HttpContent content, long contentLength)
    {
        if (response instanceof Response)
        {
            Response r = (Response)response;
            r.putHeaders(content, contentLength, _etags);
            HttpFields f = r.getHttpFields();
            if (_acceptRanges)
                f.put(ACCEPT_RANGES);
                
            if (_cacheControl != null)
                f.put(_cacheControl);
        }
        else
        {
            Response.putHeaders(response, content, contentLength, _etags);
            if (_acceptRanges)
                response.setHeader(ACCEPT_RANGES.getName(), ACCEPT_RANGES.getValue());
                
            if (_cacheControl != null)
                response.setHeader(_cacheControl.getName(), _cacheControl.getValue());
        }
    }
    
    /* ------------------------------------------------------------ */
    /*
     * @see javax.servlet.Servlet#destroy()
     */
    @Override
    public void destroy()
    {
        if (_cache != null)
            _cache.flushCache();
        super.destroy();
    }
    
}
